// Copyright 2021, pdfium-sys Developers
// Copyright 2022 - 2024, pdfium-render Developers
//
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.

#[cfg(feature = "bindings")]
extern crate bindgen;

#[cfg(feature = "bindings")]
use {
    bindgen::BindgenError,
    std::env::var,
    std::ffi::OsStr,
    std::fs::{read_dir, write},
    std::path::PathBuf,
};

#[derive(Debug)]
enum BuildError {
    #[cfg(feature = "bindings")]
    #[allow(dead_code)]
    IoError(std::io::Error),
    #[cfg(feature = "bindings")]
    #[allow(dead_code)]
    BindgenError(BindgenError),
    #[cfg(feature = "bindings")]
    #[allow(dead_code)]
    PathConversionError(PathBuf),
}

#[cfg(feature = "bindings")]
impl From<std::io::Error> for BuildError {
    fn from(item: std::io::Error) -> Self {
        BuildError::IoError(item)
    }
}

#[cfg(feature = "bindings")]
impl From<BindgenError> for BuildError {
    fn from(item: BindgenError) -> Self {
        BuildError::BindgenError(item)
    }
}

fn main() -> Result<(), BuildError> {
    #[cfg(feature = "bindings")]
    build_bindings()?;

    #[cfg(feature = "static")]
    statically_link_pdfium();

    Ok(())
}

#[cfg(feature = "bindings")]
fn build_bindings() -> Result<(), BuildError> {
    // AJRC - 3-Jan-22 - modified from the pdfium-sys version to removing explicit linking to
    // a system-provided pdfium. We still want the bindings generated by rust-bindgen,
    // since they provide various constants that are useful, but we will load functions
    // dynamically at runtime using libloading.

    // AJRC - 13-Jan-22 - docs.rs runs cargo doc in a read-only sandbox, so we can't
    // generate bindings. Skip bindings generation entirely if the DOCS_RS environment
    // variable is set, as per https://docs.rs/about/builds#detecting-docsrs.

    // AJRC - 22-Jan-22 - expanded bindings generation to cover all Pdfium modules, not
    // just the viewing and rendering functionality defined in fpdfview.h.

    // AJRC - 5-Aug-24 - expanded bindings generation to cover multiple versions of the Pdfium API.
    // Consumers can use crate feature flags to select a specific API version to use.

    for release in read_dir("include/")? {
        let release = release?.path();

        if release.is_dir() {
            build_bindings_for_one_pdfium_release(
                release
                    .file_name()
                    .ok_or(BuildError::PathConversionError(release.clone()))?
                    .to_str()
                    .ok_or(BuildError::PathConversionError(release.clone()))?,
            )?;
        }
    }

    Ok(())
}

#[cfg(feature = "bindings")]
fn build_bindings_for_one_pdfium_release(release: &str) -> Result<(), BuildError> {
    if var("DOCS_RS").is_err() {
        // The DOCS_RS environment variable is _not_ set. Proceed with bindgen.

        // Generate an import wrapper telling bindgen which header files to use as input sources.

        let header_file_extension = OsStr::new("h");
        let wrapper_file_name = OsStr::new("rust-import-wrapper.h");
        let mut included_header_files = Vec::new();

        for header in read_dir(format!("include/{}/", release))? {
            let header = header?.path();

            if header.is_file()
                && header.file_name().is_some()
                && header.extension() == Some(header_file_extension)
                && header.file_name() != Some(wrapper_file_name)
            {
                // Include this header file in the list of input sources.

                let header_file_name = header
                    .file_name()
                    .ok_or(BuildError::PathConversionError(header.clone()))?
                    .to_str()
                    .ok_or(BuildError::PathConversionError(header.clone()))?
                    .to_owned();

                included_header_files.push(header_file_name);
            }
        }

        let wrapper = included_header_files
            .iter()
            .map(|file_name| format!("#include \"{}\"", file_name))
            .collect::<Vec<_>>()
            .join("\n");

        write(
            format!("include/{}/rust-import-wrapper.h", release),
            wrapper,
        )?;

        let bindings = bindgen::Builder::default()
            // The input header we would like to generate bindings for.
            .header(format!("include/{}/rust-import-wrapper.h", release))
            .clang_arg("-DPDF_USE_SKIA") // Also generate bindings for optional SKIA functions
            .clang_arg("-D_SKIA_SUPPORT_") // (Alternative name for this setting in Pdfium 5961 and earlier)
            .clang_arg("-DPDF_ENABLE_XFA") // Also generate bindings for optional XFA functions
            .clang_arg("-DPDF_ENABLE_V8") // Also generate bindings for optional V8 functions
            .generate_cstr(true) // Recommended for Rust 1.59 and later
            .enable_function_attribute_detection()
            .size_t_is_usize(true) // There are esoteric architectures where size_t != usize. See:
            // https://github.com/rust-lang/rust-bindgen/issues/1671
            // Long term, the solution is for Bindgen to switch to converting size_t to
            // std::os::raw::c_size_t instead of usize, but c_size_t is not yet stabilized.
            .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) // Tell cargo to invalidate
            // the built crate whenever any of the included header files change.
            .clang_args(
                // Try to keep original C++ comments for docs.
                [
                    "-fretain-comments-from-system-headers",
                    "-fparse-all-comments",
                ]
                .iter(),
            )
            .generate_comments(true);

        #[cfg(feature = "pdfium_use_win32")]
        let bindings = bindings.clang_arg("-D_WIN32"); // Also generate bindings for Windows-specific functions

        // Generate the bindings to src/bindgen.rs.

        let bindings = bindings.generate()?;
        let out_path = PathBuf::from("src");

        bindings.write_to_file(out_path.join(format!("bindgen/{}.rs", release)))?;
    }

    Ok(())
}

#[cfg(feature = "static")]
fn statically_link_pdfium() {
    if let Ok(path) = std::env::var("PDFIUM_STATIC_LIB_PATH") {
        // Instruct cargo to statically link the given library during the build.

        println!("cargo:rustc-link-lib=static=pdfium");
        println!("cargo:rustc-link-search=native={}", path);

        // Optionally instruct cargo to link to a C++ standard library during the build.
        // TODO: AJRC - 30-Sep-22 - for now, we dynamically link to the selected standard library,
        // but ultimately we want to use a link type of "static:-bundle" once the feature is stabilized
        // (currently it is available only in nightly builds of Rust).

        #[cfg(feature = "libstdc++")]
        println!("cargo:rustc-link-lib=dylib=stdc++");

        #[cfg(feature = "libc++")]
        println!("cargo:rustc-link-lib=dylib=c++");
    } else if let Ok(path) = std::env::var("PDFIUM_DYNAMIC_LIB_PATH") {
        // Instruct cargo to dynamically link the given library during the build.

        println!("cargo:rustc-link-lib=dylib=pdfium");
        println!("cargo:rustc-link-search=native={}", path);
    }
}
